package cn.bbstone.pisces2.util;

import cn.bbstone.pisces2.comm.Const;
import cn.bbstone.pisces2.comm.fli.FliIndex;
import cn.bbstone.pisces2.comm.fli.FliSavePoint;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class FLIUtil {
    private static Logger log = LoggerFactory.getLogger(FLIUtil.class);
    // server & client fli directory
    private static final String SERVER_FLI_DIR = ".fli_server";
    private static final String CLIENT_FLI_DIR = ".fli_client";
    // index & savepoint file
    public static final String FLI_IDX = "fli.idx";
    private static final String FLI_SAVEPOINT = "fli.sp";

    // ---------------------------- savepoint
    /*
    public static String toSavePointJson(FliSavePoint fliSavePoint) {
        ObjectMapper objectMapper = new ObjectMapper();
        String savePointJson = null;
        try {
            savePointJson = objectMapper.writeValueAsString(fliSavePoint);
        } catch (Exception e) {
            log.error("convert logConf to JSON String error. ", e);
        }
        return savePointJson;
    }

    public static FliSavePoint fromFliJson(String savePointJson) {
        ObjectMapper objectMapper = new ObjectMapper();
        FliSavePoint fliSavePoint = null;
        try {
            fliSavePoint = objectMapper.readValue(savePointJson, FliSavePoint.class);
        } catch (Exception e) {
            log.error("convert JSON String to LogConf Object error. ", e);
        }
        return fliSavePoint;
    }*/

    public static FliSavePoint readServerSavePoint() {
        return readSavePoint(SERVER_FLI_DIR, FLI_SAVEPOINT);
    }

    public synchronized static FliSavePoint readClientSavePoint() {
        return readSavePoint(CLIENT_FLI_DIR, FLI_SAVEPOINT);
    }

    public static FliSavePoint readSavePoint(String dirName, String filename) {
        ObjectMapper objectMapper = new ObjectMapper();
        FliSavePoint fliSavePoint = null;
        try {
            FileInputStream fis = new FileInputStream(getPathDoCreateIfAbsent(dirName, filename));
            fliSavePoint = objectMapper.readValue(fis, FliSavePoint.class);
        } catch (Exception e) {
            log.error("read fli.sp fail. ", e);
        }
        return fliSavePoint;
    }

    public static void writeServerSavePoint(FliSavePoint fliSavePoint) {
        writeSavePoint(fliSavePoint, SERVER_FLI_DIR, FLI_SAVEPOINT);
    }

    // TODO add cache to write savepoint low down io to update save point file
    public synchronized static void writeClientSavePoint(FliSavePoint fliSavePoint) {
        writeSavePoint(fliSavePoint, CLIENT_FLI_DIR, FLI_SAVEPOINT);
    }

    private static void writeSavePoint(FliSavePoint fliSavePoint, String dirName, String filename) {
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            FileOutputStream fos = new FileOutputStream(getPathDoCreateIfAbsent(dirName, FLI_SAVEPOINT));
            objectMapper.writerWithDefaultPrettyPrinter().writeValue(fos, fliSavePoint);
        } catch (Exception e) {
            log.error("write fli.sp fail. ", e);
        }
    }

    public static String getServerIndexPath() {
        return getPathDoCreateIfAbsent(SERVER_FLI_DIR, FLI_IDX);
    }

    public static String getClientIndexPath() {
        return getPathDoCreateIfAbsent(CLIENT_FLI_DIR, FLI_IDX);
    }

    public static String getServerSpPath() {
        return getPathDoCreateIfAbsent(SERVER_FLI_DIR, FLI_SAVEPOINT);
    }

    public static String getClientSpPathDoCreateIfAbsent() {
        return getPathDoCreateIfAbsent(CLIENT_FLI_DIR, FLI_SAVEPOINT);
    }

    // ------------ build path string and return
    public static String getClientSpPathStr() {
        return getPath(CLIENT_FLI_DIR, FLI_SAVEPOINT);
    }
    public static String getClientIdxPathStr() {
        return getPath(CLIENT_FLI_DIR, FLI_IDX);
    }

    public static String getServerSpPathStr() {
        return getPath(SERVER_FLI_DIR, FLI_SAVEPOINT);
    }
    public static String getServerIdxPathStr() {
        return getPath(SERVER_FLI_DIR, FLI_IDX);
    }
    // -----------------------------------------------


    private static String getPath(String dirName, String filename) {
        String dir = String.format("%s%s%s", System.getProperty("user.home"), System.getProperty("file.separator"), dirName);
        String path = String.format("%s%s%s", dir, System.getProperty("file.separator"), filename);
        return path;
    }

    /**
     * @param dirName  SERVER_FLI_DIR/CLIENT_FLI_DIR
     * @param filename FLI_SAVEPOINT/FLI_IDX
     * @return
     */
    private static String getPathDoCreateIfAbsent(String dirName, String filename) {
        String dir = String.format("%s%s%s", System.getProperty("user.home"), System.getProperty("file.separator"), dirName);
        // check dir exists
        if (!Files.isDirectory(Paths.get(dir))) {
            try {
                Files.createDirectory(Paths.get(dir));
            } catch (IOException e) {
                log.error("create .fli directory error.", e);
                return null;
            }
        }
        String path = String.format("%s%s%s", dir, System.getProperty("file.separator"), filename);
        if (Files.notExists(Paths.get(path))) {
            try {
                Files.createFile(Paths.get(path));
            } catch (IOException e) {
                log.error("create fli.json file error.", e);
                return null;
            }
        }
        return path;
    }


    public static String getServerFliDir() {
        String serverFliDir = CtxUtil.getConfigModel().getServerRoot();
        if (StringUtils.isBlank(serverFliDir)) {
            serverFliDir = getServerIndexPath();
        }
        return getCanonicalPath(serverFliDir);
    }

    public static String getClientFliDir() {
        String clientFliDir = CtxUtil.getConfigModel().getClientRoot();
        if (StringUtils.isBlank(clientFliDir)) {
            clientFliDir = getClientFliDir();
        }
        return getCanonicalPath(clientFliDir);
    }


    /**
     * if path not end with File.separator, append to it before return
     *
     * @param path
     * @return
     */
    private static String getCanonicalPath(String path) {
        path = convertToLocalePath(path);
        // append last File.separator for dir
        if (Files.isDirectory(Paths.get(path))) {
            path = (path.lastIndexOf(File.separator) == (path.length() - 1)) ? path : path + File.separator;
        }
        return path;
    }

    /**
     * Do not follow symbolic links.
     *
     * LinkOption.NOFOLLOW_LINKS
     *
     * @param fullPath
     * @param basePath
     * @return
     */
    public static String getRelativePath(String fullPath, String basePath) {
        fullPath = getCanonicalPath(fullPath);
//        log.info("fullPath: {}, basePath: {}", fullPath, basePath);
        // Do not follow symbolic links.
        if (Files.notExists(Paths.get(fullPath), LinkOption.NOFOLLOW_LINKS) || !fullPath.startsWith(basePath)) {
            throw new RuntimeException(String.format("@param(filepath: %s) should be an exists client path and started with(%s).", fullPath, basePath));
        }
        return getCanonicalRelativePath(fullPath.substring(basePath.length()));
    }

    /**
     * //remove the first File.separator
     * append the fist File.separator
     *
     * @param path
     * @return
     */
    private static String getCanonicalRelativePath(String path) {
        path = convertToLocalePath(path);
        // remove the first File.separator
        /*if (path.startsWith(File.separator)) {
            path = path.substring(File.separator.length());
        }*/
        // append the fist File.separator
        if (!path.startsWith(File.separator)) {
            path = File.separator + path;
        }
        return path;
    }


    /**
     * convert path which a server os path to client os path
     * e.g. server is *nix, client is windows,
     * need to convert server path format to client os platform format
     *
     * @param path
     * @return
     */
    public static String convertToLocalePath(String path) {
        String os = System.getProperty("os.name");
        if (os.toLowerCase().startsWith("win")) {
            return path.replaceAll(Const.NIX_FILE_SEPARATOR, Matcher.quoteReplacement(File.separator));
        } else {
            return path.replaceAll(Const.WIN_FILE_SEPARATOR, File.separator);
        }
    }

    /**
     *
     * @param fileFullPath -  file full path
     */
    public static long getFileLineCount(String fileFullPath) {
        try(Stream<String> stream = Files.lines(Paths.get(fileFullPath))) {
            return stream.count();
        } catch (IOException e) {
            log.error("get file: {} lines count error.", fileFullPath);
        }
        return 0;
    }

    public static List<FliIndex> readClientIndexLines(long currentPos, int cacheSize) {
        return readIndexLines(getClientIndexPath(), currentPos, cacheSize);
    }

    public static long getClientIndexCount() {
        return getFileLineCount(getClientIndexPath());
    }

    public static long getServerIndexCount() {
        return getFileLineCount(getServerIndexPath());
    }

    /**
     * Read lines in fli.idx(include D & F line)
     *
     * @param indexFullPath
     * @param currentPos
     * @param cacheSize
     * @return
     */
    private static List<FliIndex> readIndexLines(String indexFullPath, long currentPos, int cacheSize) {
        try(Stream<String> stream = Files.lines(Paths.get(indexFullPath))) {
            // the F-file List(exclude D-dir items)
//            String fileLineFormat = "(\\d)*(" + escapeFliFieldSeparator() + ")" + Const.BFILE_CAT_FILE + ".*";
            // .filter(e -> e.matches(fileLineFormat))
            List<String> stringLines = stream.skip(currentPos).limit(cacheSize).collect(Collectors.toList());
//            log.info("stringlines: {}", stringLines.size());
            // TODO check if there is a stream way to convert from string -> array -> object
            List<FliIndex> itemList = new ArrayList<>();
            stringLines.stream().forEach(e -> itemList.add(convertToFliIndex(e)));
            return itemList;
        } catch (IOException e) {
            log.error("read index file error.", e);
        }
        return null;
    }

    private static FliIndex readIndexLine(String indexFullPath, long fileNo) {
        try(Stream<String> stream = Files.lines(Paths.get(indexFullPath))) {
            List<String> stringLines = stream.skip(fileNo - 1).limit(1).collect(Collectors.toList());
            List<FliIndex> itemList = new ArrayList<>();
            stringLines.stream().forEach(e -> itemList.add(convertToFliIndex(e)));
            return itemList == null ? null : itemList.get(0);
        } catch (IOException e) {
            log.error("read index file error.", e);
        }
        return null;
    }

    public static FliIndex readClientIndexLine(long fileNo) {
        return readIndexLine(getClientIndexPath(), fileNo);
    }

    private static String escapeFliFieldSeparator() {
        StringBuffer sb = new StringBuffer();
        for (char c : Const.FLI_FIELD_SEPARATOR.toCharArray()) {
            sb.append("\\").append(c);
        }
        return sb.toString();
    }

    public static List<FliIndex> readServerIndexLines(long currentPos, int cacheSize) {
        return readIndexLines(getServerIndexPath(), currentPos, cacheSize);
    }

    public static List<FliIndex> readServerAllIndexLines() {
        return readAllIndexLines(FLIUtil.getServerIndexPath());
    }

    public static List<FliIndex> readClientAllIndexLines() {
        return readAllIndexLines(FLIUtil.getClientIndexPath());
    }

    private static List<FliIndex> readAllIndexLines(String fliFullPath) {
        try(Stream<String> stream = Files.lines(Paths.get(fliFullPath))) {
            List<String> stringLines = stream.collect(Collectors.toList());
            List<FliIndex> itemList = new ArrayList<>();
            stringLines.stream().forEach(e -> itemList.add(convertToFliIndex(e)));
            return itemList;
        } catch (IOException e) {
            log.error("read index file error.", e);
        }
        return null;
    }


    public static FliIndex convertToFliIndex(String line) {
        FliIndex fliIndex =  new FliIndex();
        fliIndex.setFileNo(Long.valueOf(StrUtil.splitTrim(line, Const.FLI_FIELD_SEPARATOR).get(0)));
        fliIndex.setFileCat(StrUtil.splitTrim(line, Const.FLI_FIELD_SEPARATOR).get(1));
        fliIndex.setRpath(StrUtil.splitTrim(line, Const.FLI_FIELD_SEPARATOR).get(2));
        return fliIndex;
    }

    public static void main(String[] args) {
//        List<String> filter = null;
//        int size = buildServerFliIndex(BFileUtil.getServerDir(), filter);
//        log.info("root: {} has {} files.", BFileUtil.getServerDir(), size);
//        log.info("uuid: {}", IdUtil.fastSimpleUUID());
//        log.info("timestamp: {}", System.currentTimeMillis());

//        log.info(getServerIndexPath());
//        log.info("client lines: {}", getClientIndexCount());
//        log.info("server lines: {}", getServerIndexCount());
//
//        log.info("escapeSeparator: {}", escapeFliFieldSeparator());
//        List<FliIndex> list = readServerIndexLines(0, 50);
//        log.info("list.size: {}", list.size());
//        list.forEach(e -> {
//            log.info("{}, {}, {}", e.getFileNo(), e.getFileCat(), e.getRpath());
//        });

//        String clientSpPath = getClientSpPath();
//        log.info("clientSpPath: {}", clientSpPath);
//        log.info("clientSpPath.exists: {}", Files.exists(Paths.get(clientSpPath)));

        List<FliIndex> allLines = readServerAllIndexLines();
        log.info("index.count: {}, reality.count: {}", FLIUtil.getServerIndexCount(), allLines.size());
        
        List<FliIndex> allClientLines = readClientAllIndexLines();
        log.info("index.count: {}, reality.count: {}", FLIUtil.getClientIndexCount(), allClientLines.size());
    }

}
